Skip to content

Comments

스터디 목록/상세 개선#394

Merged
HA-SEUNG-JEONG merged 18 commits intodevelopfrom
fix/study
Feb 17, 2026
Merged

스터디 목록/상세 개선#394
HA-SEUNG-JEONG merged 18 commits intodevelopfrom
fix/study

Conversation

@HA-SEUNG-JEONG
Copy link
Contributor

@HA-SEUNG-JEONG HA-SEUNG-JEONG commented Feb 17, 2026

🌱 연관된 이슈

☘️ 작업 내용

스터디 목록

  • 스터디 필터 로직 개선
  • 스터디 공지 내용 글자 수 제한 500자로 변경

스터디 상세

  • 아바타 스택 추가
  • 스터디 문의 관련 컴포넌트 및 로직 작업(추후 개선 예정)
  • 모달 내 항목 선택 시 자동 스크롤 되도록 추가
  • 스터디 상세에서 리더 판별 버그 수정

🍀 참고사항

스크린샷 (선택)

스크린샷 2026-02-17 17 38 52 스크린샷 2026-02-17 17 39 09
2026-02-17.17.41.16.mov

Summary by CodeRabbit

  • 새로운 기능

    • 스터디 그룹 문의 모달(문의하기) 추가
  • 개선 사항

    • 동료 검토 작성 권한: 제출자가 아니면 누구나 작성 가능
    • 필터 UI: 선택 항목 라벨 병합 표시 및 줄바꿈 가능한 레이아웃, 포지션 레이블을 “직무”로 변경
    • 폼 UX: 입력 후 자동 스크롤로 다음 필드로 이동
    • 스터디원 표시: 아바타 스택으로 간결화, 오버플로우 처리 및 리더 표시
    • 공지 글자수 제한 확대(100→500)
    • 리스트 검색/필터/페이징 로직 중앙화로 더 일관된 동작

HA-SEUNG-JEONG and others added 12 commits February 10, 2026 21:50
스터디 상세 페이지의 아바타 스택과 신청자 목록 페이지에서 신청자들을 createdAt을 기준으로 오름차순 정렬합니다.
이를 통해 먼저 신청한 유저가 먼저 보이게 됩니다.
- useScrollToNextField 훅 추가 (data-scroll-field 속성 기반 다음 필드 자동 스크롤)
- FieldControl에 onAfterChange, onAfterBlurFilled 콜백 지원 추가
- FormField에 scrollable prop 및 data-scroll-field 마킹 지원 추가
- 스터디 개설 폼(step1, step2), 문의/과제 제출/평가 모달에 자동 스크롤 적용
- StudyInfoSection isLeader를 props로 직접 전달하여 렌더 타이밍 버그 수정
- useLeaderStore/useUserStore 의존성 제거 (homework-detail-content 포함)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
study-platform-client-dev Ready Ready Preview, Comment Feb 17, 2026 9:50am

@HA-SEUNG-JEONG
Copy link
Contributor Author

PR이 너무 길어질 거 같아서 적당히 나눠서 PR 올리겠습니다.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

문의(Inquiry) 기능과 관련 API/훅/모달을 추가하고, 폼 내 자동 스크롤(useScrollToNextField)과 FormField 확장(onAfterChange/onAfterBlurFilled/scrollable)을 도입했습니다. 스터디 목록 필터·페이징 로직은 useStudyListFilter 훅으로 중앙화했고, 참가자 UI는 AvatarStack으로 교체되었으며 숙제 리뷰 권한 로직이 단순화되었습니다.

Changes

Cohort / File(s) Summary
문의 기능 (UI/도메인/API)
src/components/modals/inquiry-modal.tsx, src/features/study/group/api/create-inquiry.ts, src/features/study/group/model/inquiry.schema.ts, src/hooks/queries/inquiry-api.ts
InquiryModal 컴포넌트, createInquiry API, zod 스키마와 useCreateInquiry 훅 추가 — 문의 생성 플로우(폼, 검증, mutation) 구현.
폼 자동 스크롤·폼필드 확장
src/hooks/use-scroll-to-next-field.ts, src/components/ui/form/field-control.tsx, src/components/ui/form/form-field.tsx
새 SCROLL_FIELD_ATTR 및 useScrollToNextField 훅 추가. FieldControl에 onAfterChange/onAfterBlurFilled 추가, FormField에 scrollable·onAfter... 전달으로 자동 스크롤 연계 가능.
그룹 스터디 스텝 폼 적용
src/features/study/group/ui/step/step1-group.tsx, src/features/study/group/ui/step/step2-group.tsx
여러 FormField에 scrollable 및 onAfterBlurFilled/onAfterChange 연결. 스크롤 대상 마크업(SCROLL_FIELD_ATTR) 삽입 및 scrollToNext 호출 추가.
스터디 목록 훅화·페이지네이션 외부화
src/hooks/common/use-study-list-filter.ts, src/components/pages/group-study-list-page.tsx, src/components/pages/premium-study-list-page.tsx, src/components/premium/premium-study-pagination.tsx, src/features/study/group/ui/group-study-pagination.tsx
useStudyListFilter 훅 도입으로 검색/필터/페이징 로직 중앙화. Pagination 컴포넌트들에 onPageChange prop 추가해 라우터 의존 제거.
참가자 UI 재설계 및 AvatarStack 도입
src/components/ui/avatar-stack.tsx, src/components/section/group-study-info-section.tsx, src/components/section/premium-study-info-section.tsx, src/features/my-page/ui/applicant-page.tsx
새 AvatarStack 컴포넌트 및 AvatarStackMember 타입 추가. 기존 개별 applicant 렌더를 AvatarStack으로 대체하고 신청자 정렬을 createdAt 기준으로 적용.
모달·제출 폼 조정 및 평가 모달 연동
src/components/modals/submit-homework-modal.tsx, src/components/modals/create-evaluation-modal.tsx, src/components/contents/homework-detail-content.tsx
SubmitHomeworkModal을 FormProvider 기반으로 리팩토링(부모 폼 reset). 평가 모달에 scroll-to-next 적용. homework-detail에서 리뷰 권한을 제출자 제외(!isMyHomework)로 단순화.
필터·요약·공지 등 소규모 변경
src/components/filtering/study-filter.tsx, src/components/summary/study-info-summary.tsx, src/features/study/group/ui/group-notice-modal.tsx
필터 드롭다운에 선택 라벨 결합 표시, 포지션 레이블 텍스트 변경("포지션"→"직무"), 요약 레이블 변경("현직자 참여 여부"→"스터디 대상"), 공지 입력 최대 길이 100→500으로 확장.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as InquiryModal
    participant Form as ReactHookForm
    participant Hook as useCreateInquiry
    participant API as createInquiry (POST)
    participant Toast as Toast

    User->>UI: 모달 열기
    User->>Form: category/title/content 입력
    User->>UI: 제출 클릭
    UI->>Form: zod 검증 (resolver)
    Form-->>UI: 유효성 통과
    UI->>Hook: mutate({ groupStudyId, request })
    Hook->>API: POST /group-studies/{id}/questions
    API-->>Hook: 성공 응답
    Hook-->>UI: mutation success
    UI->>Toast: 성공 토스트 표시
    UI->>Form: reset()
    UI->>User: 모달 닫기
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 스크롤 쓱쓱 다음 칸으로 이동하면,
문의는 휙 서버로 가서 답을 기다리네.
아바타 줄 맞춰 리더는 왕관쓰고,
필터는 깔끔히 골라 보여주네.
당근 한 줌으로 오늘도 배포 축하! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '스터디 목록/상세 개선'은 변경사항의 핵심을 정확히 요약합니다. 아바타 스택 추가, 스터디 문의 기능, 스크롤 동작 개선, 필터 로직 개선 등 여러 개선사항을 포괄적으로 나타냅니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/study

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@HA-SEUNG-JEONG HA-SEUNG-JEONG changed the title Fix/study 스터디 목록/상세 개선 Feb 17, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/summary/study-info-summary.tsx (1)

104-110: ⚠️ Potential issue | 🟡 Minor

basicInfo ?? {}로 인한 잠재적 런타임 오류.

basicInfonull/undefined일 경우 ?? {}에 의해 빈 객체가 할당되며, experienceLevelsundefined가 됩니다. Line 107의 .map() 호출 시 TypeError가 발생합니다. 같은 문제가 targetRoles (Line 82), price (Line 90) 등에도 존재합니다.

이 파일의 변경은 아니지만, 변경된 라벨과 동일 블록에 있으므로 확인이 필요합니다.

🛡️ 방어적 처리 예시
     {
-      label: '스터디 대상',
+      label: '스터디 대상',  
       value:
-        experienceLevels
+        (experienceLevels ?? [])
           .map((level) => EXPERIENCE_LEVEL_LABELS[level])
           .join(', ') || '무관',
     },

targetRoles (Line 82)에도 동일한 패턴 적용을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/summary/study-info-summary.tsx` around lines 104 - 110, The
code uses basicInfo ?? {} which can leave nested fields like experienceLevels
and targetRoles undefined and cause .map() TypeError; update the destructuring
or access sites in the StudyInfoSummary component so experienceLevels and
targetRoles default to empty arrays and price defaults to a safe value (e.g.,
null or 0) when basicInfo is nullish—for example, when pulling from basicInfo
ensure you write something equivalent to const { experienceLevels = [],
targetRoles = [], price = null } = basicInfo ?? {}; then use
EXPERIENCE_LEVEL_LABELS and ROLE_LABELS lookups as before so .map() and .join()
never run on undefined.
src/components/ui/form/form-field.tsx (1)

100-106: 🛠️ Refactor suggestion | 🟠 Major

'data-scroll-field' 문자열이 하드코딩되어 있습니다.

use-scroll-to-next-field.ts에 이미 SCROLL_FIELD_ATTR 상수가 정의되어 있습니다. 상수를 import하여 사용하면 추후 속성명 변경 시 불일치를 방지할 수 있습니다.

🔧 수정 제안
+import { SCROLL_FIELD_ATTR } from '@/hooks/use-scroll-to-next-field';
 // ...
     <div
-      {...(scrollable ? { 'data-scroll-field': name } : {})}
+      {...(scrollable ? { [SCROLL_FIELD_ATTR]: name } : {})}
       className={cn(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/form/form-field.tsx` around lines 100 - 106, Replace the
hardcoded 'data-scroll-field' attribute in the FormField component with the
shared constant from use-scroll-to-next-field.ts: import SCROLL_FIELD_ATTR and
use it in the conditional spread ({ [SCROLL_FIELD_ATTR]: name } when scrollable)
inside the return JSX of the FormField (the block that currently spreads
{'data-scroll-field': name}); this ensures FormField uses the canonical
SCROLL_FIELD_ATTR constant used by use-scroll-to-next-field.
🧹 Nitpick comments (12)
src/components/summary/study-info-summary.tsx (1)

21-21: 상대 경로 대신 @/ 경로 별칭 사용을 권장합니다.

코딩 가이드라인에 따르면 @/*./src/*에 매핑하여 사용해야 합니다.

-} from '../../features/study/group/const/group-study-const';
+} from '@/features/study/group/const/group-study-const';

As per coding guidelines: "Use path alias @/* to map to ./src/* as configured in tsconfig.json for cleaner imports"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/summary/study-info-summary.tsx` at line 21, The import uses a
relative path '../../features/study/group/const/group-study-const'—update it to
the project path alias by replacing the relative path with
'@/features/study/group/const/group-study-const' so it maps to ./src/* per
tsconfig.json; ensure any other imports in study-info-summary.tsx follow the
same `@/` alias convention and that the TypeScript compiler/ESLint resolves the
alias correctly.
src/features/study/group/ui/group-notice-modal.tsx (1)

117-119: 폼 ID를 용도에 맞게 변경하세요

id="study-done-form"은 공지사항(notice) 폼에 사용되고 있으나, 실제로는 src/features/study/interview/ui/study-done-modal.tsx의 완료 폼에도 동일한 ID가 존재합니다. 현재 두 모달이 서로 다른 페이지에서 사용되어 즉시 충돌하지는 않지만, 같은 ID 사용으로 인해 코드의 의도가 불명확하고 향후 같은 페이지에 함께 렌더링될 경우 DOM 충돌이 발생할 수 있습니다.

ID를 study-notice-form으로 변경하여 의도를 명확히 하세요. 라인 156의 form="study-done-form" 속성도 함께 업데이트해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/study/group/ui/group-notice-modal.tsx` around lines 117 - 119,
The form id "study-done-form" in the GroupNoticeModal should be renamed to
"study-notice-form" to reflect its purpose; update the id attribute on the
<form> (currently id="study-done-form") and also update the corresponding
form="study-done-form" reference elsewhere in the component (the attribute that
ties the submit button to the form) so both use "study-notice-form" (look for
the GroupNoticeModal / group-notice-modal component and the attributes
id="study-done-form" and form="study-done-form").
src/components/filtering/study-filter.tsx (1)

88-102: clsx 또는 tailwind-merge 사용을 고려해 보세요.

.join(' ') 패턴 대신 프로젝트 코딩 가이드라인에 명시된 clsx/tailwind-merge 유틸리티를 사용하면 조건부 클래스 결합이 더 깔끔해집니다. 기존 코드에서도 동일한 패턴이 사용되고 있으므로, 별도 리팩토링 PR에서 일괄 변경하는 것이 나을 수 있습니다.

♻️ clsx 적용 예시
+import clsx from 'clsx';
...
         <button
           type="button"
-          className={[
-            'flex items-center gap-50 rounded-full border px-200 py-100 whitespace-nowrap',
-            hasSelection
-              ? 'border-border-brand bg-fill-brand-subtle-default text-text-brand'
-              : 'border-border-default bg-fill-neutral-subtle-default text-text-default',
-          ].join(' ')}
+          className={clsx(
+            'flex items-center gap-50 rounded-full border px-200 py-100 whitespace-nowrap',
+            hasSelection
+              ? 'border-border-brand bg-fill-brand-subtle-default text-text-brand'
+              : 'border-border-default bg-fill-neutral-subtle-default text-text-default',
+          )}
         >

As per coding guidelines, src/**/*.{ts,tsx}: "Style components using Tailwind CSS 4 with @tailwindcss/postcss plugin, clsx, tailwind-merge, and class-variance-authority (CVA) utilities".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/filtering/study-filter.tsx` around lines 88 - 102, Replace the
manual array + .join(' ') className construction on the filter toggle button
with the project's recommended utility (clsx or tailwind-merge): locate the
button in src/components/filtering/study-filter.tsx that uses the array
combining classes based on hasSelection and open (the element rendering
displayLabel and ChevronUp/ChevronDown) and change its className to call
clsx(...) or twMerge(clsx(...)) so conditional classes are composed via the
utility instead of .join; ensure you import the chosen utility at the top of the
file and preserve the exact conditional branches (hasSelection ? ... : ...) and
existing static classes.
src/components/premium/premium-study-pagination.tsx (1)

1-24: GroupStudyPagination과 거의 동일한 구조입니다.

두 페이지네이션 컴포넌트(GroupStudyPagination, PremiumStudyPagination)가 className 차이(mt-400 py-200 vs mt-600)만 남았습니다. 하나의 공통 컴포넌트로 통합하고 className을 prop으로 받는 것을 고려해 보세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/premium/premium-study-pagination.tsx` around lines 1 - 24,
Both GroupStudyPagination and PremiumStudyPagination duplicate logic; refactor
by creating a single configurable component (e.g., StudyPagination) that wraps
the existing Pagination component and accepts props: currentPage, totalPages,
onPageChange, and className; update GroupStudyPagination and
PremiumStudyPagination to either be thin wrappers that pass their specific
className (e.g., "mt-400 py-200" and "mt-600") to StudyPagination or replace
usages directly with StudyPagination, ensuring the wrapped Pagination props
(page, totalPages, onChangePage) are passed through unchanged.
src/components/pages/premium-study-list-page.tsx (1)

24-36: group-study-list-page.tsx와 동일한 패턴으로 훅이 잘 적용되었습니다.

두 리스트 페이지의 구조가 거의 동일합니다. 향후 공통 레이아웃 컴포넌트로 추출하여 classification, 제목, 리스트/페이지네이션 컴포넌트를 prop으로 받는 방식도 고려해 볼 수 있습니다. 현재 규모에서는 이 정도 중복은 수용 가능합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/pages/premium-study-list-page.tsx` around lines 24 - 36, 두 리스트
페이지가 동일한 구조를 반복하므로 중복을 줄이기 위해 공통 레이아웃 컴포넌트로 추출하세요: 현재
premium-study-list-page.tsx와 group-study-list-page.tsx에서 사용 중인
useStudyListFilter 훅(예: 호출부 with classification: 'PREMIUM_STUDY' 등), 리스트 렌더링(예:
displayStudies, isLoading), 검색/필터/페이징 핸들러(handleSearch, handleFilterChange,
handlePageChange)와 제목/클래스명을 props로 받아 재사용 가능한 StudyListLayout 컴포넌트를 만들고, 각 페이지는
해당 훅 호출과 props 전달만 하도록 리팩토링하세요.
src/hooks/common/use-study-list-filter.ts (1)

31-48: 검색 시 pageSize: 10000으로 전체 데이터를 가져오는 방식은 확장성 우려가 있습니다.

검색어가 있을 때 모든 스터디를 한 번에 가져와 클라이언트에서 필터링하는 구조입니다. 데이터가 증가하면 응답 크기와 메모리 사용량이 문제가 될 수 있습니다. 백엔드 검색 API가 준비되면 서버 사이드 검색으로 전환하는 것이 바람직합니다.

현재 단계에서는 백엔드 API 부재로 인한 임시 방편으로 이해되지만, TODO 주석을 남겨두면 향후 리팩토링 시 참고가 됩니다.

💡 TODO 주석 추가 제안
  const { data, isLoading } = useGetStudies({
    classification,
    page: searchQuery ? 1 : currentPage,
-   pageSize: searchQuery ? 10000 : PAGE_SIZE,
+   // TODO: 백엔드 검색 API가 준비되면 서버 사이드 검색으로 전환 필요
+   pageSize: searchQuery ? 10000 : PAGE_SIZE,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/common/use-study-list-filter.ts` around lines 31 - 48, The code
forces pageSize: 10000 when searchQuery is present which is a temporary
client-side workaround that will not scale; update the useGetStudies call (the
pageSize ternary and related logic around searchQuery, currentPage, PAGE_SIZE,
and useGetStudies) by adding a clear TODO comment above this block stating that
this is a temporary client-side full-fetch for search and that it must be
replaced with server-side search once the backend API is available, including
expected acceptance criteria (e.g., remove the 10000 hack, use paginated server
search, and revert to PAGE_SIZE), so future maintainers know why it exists and
what to change.
src/hooks/use-scroll-to-next-field.ts (1)

10-21: document.querySelectorAll이 전체 문서를 대상으로 동작합니다.

현재 구현은 페이지 전체에서 data-scroll-field 요소를 검색하므로, 여러 폼이나 모달이 동시에 렌더링되면 의도하지 않은 필드로 스크롤될 수 있습니다. 현재 사용 패턴(모달/폼이 하나만 열림)에서는 문제없지만, 향후 확장 시 컨테이너 ref를 받아 스코프를 제한하는 방식을 고려해 보세요.

// 예: 스코프를 제한하는 확장 패턴
export function useScrollToNextField(containerRef?: React.RefObject<HTMLElement>) {
  return useCallback((currentFieldName: string) => {
    const root = containerRef?.current ?? document;
    const all = root.querySelectorAll(`[${SCROLL_FIELD_ATTR}]`);
    // ...
  }, [containerRef]);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/use-scroll-to-next-field.ts` around lines 10 - 21, The hook
useScrollToNextField currently queries the whole document via
document.querySelectorAll which can cross-scope forms/modals; change its
signature to accept an optional containerRef (React.RefObject<HTMLElement>),
inside the callback use const root = containerRef?.current ?? document and call
root.querySelectorAll(`[${SCROLL_FIELD_ATTR}]`) instead of
document.querySelectorAll, update the callback dependency array to include
containerRef, and preserve existing behavior for currentFieldName and the
scrollIntoView logic while keeping element casting to HTMLElement.
src/components/ui/form/field-control.tsx (1)

94-99: field.value의 truthy 체크가 falsy 값(0, false)을 "미입력"으로 처리합니다.

Line 97의 if (field.value) 조건은 값이 0이나 false인 경우에도 onAfterBlurFilled을 호출하지 않습니다. 현재 텍스트 입력 중심의 사용에서는 문제없지만, 숫자 필드 등에서 재사용 시 의도치 않은 동작이 발생할 수 있습니다.

🔧 보다 안전한 체크
          const nextOnBlur = onAfterBlurFilled
            ? () => {
                baseOnBlur();
-               if (field.value) onAfterBlurFilled();
+               if (field.value !== undefined && field.value !== '' && field.value !== null) onAfterBlurFilled();
              }
            : baseOnBlur;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/form/field-control.tsx` around lines 94 - 99, The current
truthy check in the nextOnBlur handler incorrectly treats valid falsy values
like 0 or false as "empty"; update the condition that decides whether to call
onAfterBlurFilled to explicitly check for null/undefined/empty-string instead of
using if (field.value). Locate nextOnBlur (which currently references
baseOnBlur, onAfterBlurFilled and field.value) and replace the truthy check with
an explicit guard such as ensuring field.value is not null/undefined and not the
empty string (e.g., field.value != null && field.value !== '') before calling
onAfterBlurFilled.
src/hooks/queries/inquiry-api.ts (1)

7-20: onSuccess에서 관련 쿼리 무효화가 누락되었습니다.

코딩 가이드라인에 따르면, useMutation 훅에서는 onSuccess 콜백에서 queryClient.invalidateQueries를 호출하여 관련 쿼리를 무효화해야 합니다. 현재는 문의 목록 조회 기능이 없을 수 있지만, 향후 추가될 때를 대비하여 무효화 로직을 미리 설정하는 것이 좋습니다.

♻️ 수정 제안
+import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { useMutation } from '@tanstack/react-query';
 import {
   createInquiry,
   CreateInquiryRequest,
 } from '@/features/study/group/api/create-inquiry';

 export const useCreateInquiry = () => {
+  const queryClient = useQueryClient();
+
   return useMutation({
     mutationFn: async ({
       groupStudyId,
       request,
     }: {
       groupStudyId: number;
       request: CreateInquiryRequest;
     }) => {
       const data = await createInquiry(groupStudyId, request);

       return data.content;
     },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['inquiries'] });
+    },
   });
 };

As per coding guidelines, "For useMutation hooks, call queryClient.invalidateQueries in onSuccess to invalidate related queries using parent keys (e.g., ['missions'] for all mission queries)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/queries/inquiry-api.ts` around lines 7 - 20, The useCreateInquiry
mutation currently returns the created inquiry but does not invalidate related
queries on success; update the useCreateInquiry hook to add an onSuccess
callback that calls queryClient.invalidateQueries with the appropriate parent
key(s) (e.g., ['inquiries'] or the parent resource key) after createInquiry
resolves; locate the useCreateInquiry function and modify its useMutation
options to include onSuccess: (data, variables) => {
queryClient.invalidateQueries(['inquiries', variables.groupStudyId]) } or a
suitable parent key so related inquiry lists are refreshed.
src/components/section/premium-study-info-section.tsx (1)

46-60: useMemo의 의존성 배열이 매 렌더마다 새로운 참조를 가집니다.

applicantsList는 매 렌더마다 getApplicantsList()를 통해 새 배열이 생성되므로, useMemo[applicantsList] 의존성이 매번 변경된 것으로 인식됩니다. 메모이제이션이 효과가 없습니다.

approvedApplicants?.pages를 의존성으로 사용하는 것이 적절합니다.

♻️ 수정 제안
   const avatarMembers = useMemo<AvatarStackMember[]>(() => {
+    const applicantsList = getApplicantsList(approvedApplicants?.pages);
     return [...applicantsList]
       .sort(
         (a, b) =>
           new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
       )
       .map((data) => ({
         memberId: data.applicantInfo.memberId,
         nickname: data.applicantInfo.memberNickname || '익명',
         profileImageUrl:
           data.applicantInfo.profileImage?.resizedImages[0]?.resizedImageUrl ??
           '',
         isLeader: data.role === 'LEADER',
       }));
-  }, [applicantsList]);
+  }, [approvedApplicants?.pages]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/section/premium-study-info-section.tsx` around lines 46 - 60,
The useMemo for avatarMembers is using applicantsList which is a new array
reference every render (from getApplicantsList()), so memoization never holds;
change the dependency to a stable source such as approvedApplicants?.pages (or
derive a stable key from it) and keep the mapping logic inside the same
avatarMembers useMemo so the memoization actually works; update the dependency
array of useMemo( /* avatarMembers */ , [approvedApplicants?.pages]) and remove
applicantsList as a dependency.
src/features/study/group/api/create-inquiry.ts (1)

22-33: try/catch에서 에러를 로깅하고 다시 throw하는 것은 불필요합니다.

에러를 console.error로 로깅한 후 다시 throw하고 있는데, 호출측인 inquiry-modal.tsx에서도 console.error로 동일한 에러를 로깅합니다. 이로 인해 에러가 이중 로깅됩니다. API 레이어에서는 에러를 그대로 전파하고, 로깅은 호출측에 위임하는 것이 깔끔합니다.

♻️ 수정 제안
 export const createInquiry = async (
   groupStudyId: number,
   request: CreateInquiryRequest,
 ) => {
-  try {
-    const { data } = await axiosInstance.post<CreateInquiryResponse>(
-      `/group-studies/${groupStudyId}/questions`,
-      request,
-    );
-
-    return data;
-  } catch (error) {
-    console.error('문의 작성 실패:', error);
-    throw error;
-  }
+  const { data } = await axiosInstance.post<CreateInquiryResponse>(
+    `/group-studies/${groupStudyId}/questions`,
+    request,
+  );
+
+  return data;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/study/group/api/create-inquiry.ts` around lines 22 - 33, The API
function in create-inquiry.ts currently wraps the axiosInstance.post call in a
try/catch, logs the error with console.error, and rethrows it, causing duplicate
logging upstream; remove the try/catch block in the exported function (the one
calling
axiosInstance.post<CreateInquiryResponse>(`/group-studies/${groupStudyId}/questions`,
request)) so the error is not logged here and is allowed to propagate to the
caller (inquiry-modal.tsx) which will handle/log it.
src/components/modals/inquiry-modal.tsx (1)

86-98: 모달이 닫혀있을 때도 Portal이 항상 렌더링됩니다.

UserProfileModal에서는 {open && <Modal.Portal>...}으로 조건부 렌더링을 하고 있지만, 이 컴포넌트에서는 Modal.Portal이 항상 렌더링됩니다. Radix Dialog 기반이라면 내부적으로 처리될 수 있지만, 불필요한 DOM 노드 생성을 방지하려면 조건부 렌더링을 고려해주세요.

♻️ 수정 제안
   <Modal.Root open={open} onOpenChange={handleOpenChange}>
+    {open && (
       <Modal.Portal>
         <Modal.Overlay />
         <Modal.Content size="medium" className="w-[500px]">
           {/* ... content ... */}
         </Modal.Content>
       </Modal.Portal>
+    )}
   </Modal.Root>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/modals/inquiry-modal.tsx` around lines 86 - 98, The
Modal.Portal is always rendered even when the dialog is closed; wrap the Portal
(the JSX subtree starting with Modal.Portal) in a conditional so it only mounts
when open is true (e.g., change the return to render {open && <Modal.Portal>...}
instead of always rendering Modal.Portal), keeping the existing Modal.Root (open
and onOpenChange/handleOpenChange) and other Modal.* components unchanged so you
avoid creating unnecessary DOM nodes when the modal is closed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/section/group-study-info-section.tsx`:
- Line 34: The code only reads the first page via
approvedApplicants?.pages[0]?.content, omitting subsequent pages; change to
aggregate all pages' content (e.g., flatMap approvedApplicants.pages to collect
every page.content) or reuse the existing helper getApplicantsList() logic from
premium-study-info-section.tsx so applicants includes all paginated entries;
update any usage of the applicants variable accordingly to operate on the
flattened list.
- Around line 53-58: The nickname mapping currently prepends a crown emoji for
leaders in the sortedApplicants.map block (using data.role === 'LEADER'), which
causes duplicate crowns because AvatarStack already renders a crown for leaders;
remove the crown prefix so nickname becomes simply
data.applicantInfo.memberNickname || '익명' for all roles while keeping memberId
unchanged, letting AvatarStack (avatar-stack component) handle leader adornment.

In `@src/components/section/premium-study-info-section.tsx`:
- Around line 52-54: The mapping of nicknames is inconsistent:
premium-study-info-section.tsx maps nickname from applicantInfo.memberNickname
(no crown) while group-study-info-section.tsx prepends '👑' for leaders, and
AvatarStack already renders the crown icon; remove the manual '👑' prefix in
group-study-info-section.tsx so both components map nickname the same way (use
applicantInfo.memberNickname || '익명') and let AvatarStack handle leader
rendering; update any leader-check logic that previously relied on the prefixed
string to use a separate leader flag instead.

In `@src/components/ui/avatar-stack.tsx`:
- Line 29: The overflowRef declared in the AvatarStack component is never used
to detect outside clicks; either implement a click-outside handler that closes
the avatar overflow panel or remove the unused ref. Fix by wiring overflowRef to
the overflow container and adding a useEffect that registers a
mousedown/touchstart listener which checks if overflowRef.current exists and
does NOT contain the event.target, then calls the panel close function (e.g.,
setIsOpen(false) or onClose handler used by AvatarStack); ensure the listener is
removed in cleanup. Alternatively, if no outside-close UX is desired, delete
overflowRef and any related unused imports to avoid dead code.
- Around line 158-169: The Tooltip is being conditionally mounted by the
external hovered state and also receives a styled div via the trigger prop,
causing duplicate text and breaking Radix UI's built-in hover handling; remove
the external hovered conditional and the separate trigger div, and instead pass
the avatar element itself as the Tooltip's trigger so Radix can manage hover
lifecycles; update usages around Tooltip, hovered, and member.nickname in
AvatarStack (or the component containing the avatar) to stop toggling Tooltip
mount with hovered and rely on Tooltip's internal hover behavior so content
isn't duplicated and the tooltip doesn't unmount when moving the pointer from
avatar to tooltip.

In `@src/features/study/group/model/inquiry.schema.ts`:
- Around line 14-21: The title field in the inquiry Zod schema has a duplicated
.min(1) call; remove the redundant .min(1) (the second one) from the z.string()
chain so only .min(1, '제목을 입력해주세요.') remains, preserving the custom error
message and keeping the existing .max(INQUIRY_TITLE_MAX_LENGTH, ...) validation.

In `@src/hooks/common/use-study-list-filter.ts`:
- Around line 61-63: handleSearch currently only calls setSearchQuery so if the
user is on a later page a new search can yield no visible results; update the
handleSearch callback to also reset pagination by calling setCurrentPage(1)
(alongside setSearchQuery(query)) so displayStudies slicing (startIndex =
(currentPage-1) * 15) uses page 1 for fresh searches; modify the useCallback
that defines handleSearch to include setCurrentPage in its body and dependency
array as needed.
- Line 50: The code treats the return of useGetStudies as an object with a
content field, but useGetStudies' queryFn already returns the content array
itself; update the memo that defines allStudies to use data ?? [] (i.e., change
data?.content ?? [] to data ?? []) and remove or refactor any direct accesses to
data?.totalPages (e.g., the reference at the symbol where totalPages is used) —
either obtain pagination metadata from a revised useGetStudies return shape or
manage pagination separately; ensure references to useGetStudies, allStudies,
and any totalPages usage are updated consistently.

---

Outside diff comments:
In `@src/components/summary/study-info-summary.tsx`:
- Around line 104-110: The code uses basicInfo ?? {} which can leave nested
fields like experienceLevels and targetRoles undefined and cause .map()
TypeError; update the destructuring or access sites in the StudyInfoSummary
component so experienceLevels and targetRoles default to empty arrays and price
defaults to a safe value (e.g., null or 0) when basicInfo is nullish—for
example, when pulling from basicInfo ensure you write something equivalent to
const { experienceLevels = [], targetRoles = [], price = null } = basicInfo ??
{}; then use EXPERIENCE_LEVEL_LABELS and ROLE_LABELS lookups as before so .map()
and .join() never run on undefined.

In `@src/components/ui/form/form-field.tsx`:
- Around line 100-106: Replace the hardcoded 'data-scroll-field' attribute in
the FormField component with the shared constant from
use-scroll-to-next-field.ts: import SCROLL_FIELD_ATTR and use it in the
conditional spread ({ [SCROLL_FIELD_ATTR]: name } when scrollable) inside the
return JSX of the FormField (the block that currently spreads
{'data-scroll-field': name}); this ensures FormField uses the canonical
SCROLL_FIELD_ATTR constant used by use-scroll-to-next-field.

---

Nitpick comments:
In `@src/components/filtering/study-filter.tsx`:
- Around line 88-102: Replace the manual array + .join(' ') className
construction on the filter toggle button with the project's recommended utility
(clsx or tailwind-merge): locate the button in
src/components/filtering/study-filter.tsx that uses the array combining classes
based on hasSelection and open (the element rendering displayLabel and
ChevronUp/ChevronDown) and change its className to call clsx(...) or
twMerge(clsx(...)) so conditional classes are composed via the utility instead
of .join; ensure you import the chosen utility at the top of the file and
preserve the exact conditional branches (hasSelection ? ... : ...) and existing
static classes.

In `@src/components/modals/inquiry-modal.tsx`:
- Around line 86-98: The Modal.Portal is always rendered even when the dialog is
closed; wrap the Portal (the JSX subtree starting with Modal.Portal) in a
conditional so it only mounts when open is true (e.g., change the return to
render {open && <Modal.Portal>...} instead of always rendering Modal.Portal),
keeping the existing Modal.Root (open and onOpenChange/handleOpenChange) and
other Modal.* components unchanged so you avoid creating unnecessary DOM nodes
when the modal is closed.

In `@src/components/pages/premium-study-list-page.tsx`:
- Around line 24-36: 두 리스트 페이지가 동일한 구조를 반복하므로 중복을 줄이기 위해 공통 레이아웃 컴포넌트로 추출하세요: 현재
premium-study-list-page.tsx와 group-study-list-page.tsx에서 사용 중인
useStudyListFilter 훅(예: 호출부 with classification: 'PREMIUM_STUDY' 등), 리스트 렌더링(예:
displayStudies, isLoading), 검색/필터/페이징 핸들러(handleSearch, handleFilterChange,
handlePageChange)와 제목/클래스명을 props로 받아 재사용 가능한 StudyListLayout 컴포넌트를 만들고, 각 페이지는
해당 훅 호출과 props 전달만 하도록 리팩토링하세요.

In `@src/components/premium/premium-study-pagination.tsx`:
- Around line 1-24: Both GroupStudyPagination and PremiumStudyPagination
duplicate logic; refactor by creating a single configurable component (e.g.,
StudyPagination) that wraps the existing Pagination component and accepts props:
currentPage, totalPages, onPageChange, and className; update
GroupStudyPagination and PremiumStudyPagination to either be thin wrappers that
pass their specific className (e.g., "mt-400 py-200" and "mt-600") to
StudyPagination or replace usages directly with StudyPagination, ensuring the
wrapped Pagination props (page, totalPages, onChangePage) are passed through
unchanged.

In `@src/components/section/premium-study-info-section.tsx`:
- Around line 46-60: The useMemo for avatarMembers is using applicantsList which
is a new array reference every render (from getApplicantsList()), so memoization
never holds; change the dependency to a stable source such as
approvedApplicants?.pages (or derive a stable key from it) and keep the mapping
logic inside the same avatarMembers useMemo so the memoization actually works;
update the dependency array of useMemo( /* avatarMembers */ ,
[approvedApplicants?.pages]) and remove applicantsList as a dependency.

In `@src/components/summary/study-info-summary.tsx`:
- Line 21: The import uses a relative path
'../../features/study/group/const/group-study-const'—update it to the project
path alias by replacing the relative path with
'@/features/study/group/const/group-study-const' so it maps to ./src/* per
tsconfig.json; ensure any other imports in study-info-summary.tsx follow the
same `@/` alias convention and that the TypeScript compiler/ESLint resolves the
alias correctly.

In `@src/components/ui/form/field-control.tsx`:
- Around line 94-99: The current truthy check in the nextOnBlur handler
incorrectly treats valid falsy values like 0 or false as "empty"; update the
condition that decides whether to call onAfterBlurFilled to explicitly check for
null/undefined/empty-string instead of using if (field.value). Locate nextOnBlur
(which currently references baseOnBlur, onAfterBlurFilled and field.value) and
replace the truthy check with an explicit guard such as ensuring field.value is
not null/undefined and not the empty string (e.g., field.value != null &&
field.value !== '') before calling onAfterBlurFilled.

In `@src/features/study/group/api/create-inquiry.ts`:
- Around line 22-33: The API function in create-inquiry.ts currently wraps the
axiosInstance.post call in a try/catch, logs the error with console.error, and
rethrows it, causing duplicate logging upstream; remove the try/catch block in
the exported function (the one calling
axiosInstance.post<CreateInquiryResponse>(`/group-studies/${groupStudyId}/questions`,
request)) so the error is not logged here and is allowed to propagate to the
caller (inquiry-modal.tsx) which will handle/log it.

In `@src/features/study/group/ui/group-notice-modal.tsx`:
- Around line 117-119: The form id "study-done-form" in the GroupNoticeModal
should be renamed to "study-notice-form" to reflect its purpose; update the id
attribute on the <form> (currently id="study-done-form") and also update the
corresponding form="study-done-form" reference elsewhere in the component (the
attribute that ties the submit button to the form) so both use
"study-notice-form" (look for the GroupNoticeModal / group-notice-modal
component and the attributes id="study-done-form" and form="study-done-form").

In `@src/hooks/common/use-study-list-filter.ts`:
- Around line 31-48: The code forces pageSize: 10000 when searchQuery is present
which is a temporary client-side workaround that will not scale; update the
useGetStudies call (the pageSize ternary and related logic around searchQuery,
currentPage, PAGE_SIZE, and useGetStudies) by adding a clear TODO comment above
this block stating that this is a temporary client-side full-fetch for search
and that it must be replaced with server-side search once the backend API is
available, including expected acceptance criteria (e.g., remove the 10000 hack,
use paginated server search, and revert to PAGE_SIZE), so future maintainers
know why it exists and what to change.

In `@src/hooks/queries/inquiry-api.ts`:
- Around line 7-20: The useCreateInquiry mutation currently returns the created
inquiry but does not invalidate related queries on success; update the
useCreateInquiry hook to add an onSuccess callback that calls
queryClient.invalidateQueries with the appropriate parent key(s) (e.g.,
['inquiries'] or the parent resource key) after createInquiry resolves; locate
the useCreateInquiry function and modify its useMutation options to include
onSuccess: (data, variables) => { queryClient.invalidateQueries(['inquiries',
variables.groupStudyId]) } or a suitable parent key so related inquiry lists are
refreshed.

In `@src/hooks/use-scroll-to-next-field.ts`:
- Around line 10-21: The hook useScrollToNextField currently queries the whole
document via document.querySelectorAll which can cross-scope forms/modals;
change its signature to accept an optional containerRef
(React.RefObject<HTMLElement>), inside the callback use const root =
containerRef?.current ?? document and call
root.querySelectorAll(`[${SCROLL_FIELD_ATTR}]`) instead of
document.querySelectorAll, update the callback dependency array to include
containerRef, and preserve existing behavior for currentFieldName and the
scrollIntoView logic while keeping element casting to HTMLElement.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/components/section/group-study-info-section.tsx (1)

40-65: 리더 우선 정렬이 AvatarStack 내부와 중복됩니다.

avatarMembers에서 리더를 먼저 배치하고 createdAt 기준으로 정렬하고 있으나, AvatarStack 컴포넌트 내부(avatar-stack.tsx Line 48-53)에서도 isLeader 기준으로 다시 정렬합니다.

createdAt 기반 세부 정렬은 이 컴포넌트에서만 알 수 있는 정보이므로 여기서 수행하는 것이 맞지만, 리더를 앞으로 배치하는 로직은 AvatarStack에 위임하고 여기서는 createdAt 정렬만 수행하면 됩니다.

♻️ 정렬 책임 분리 제안
  const avatarMembers = useMemo<AvatarStackMember[]>(() => {
    if (!applicants.length) return [];

-    const leader = applicants.find((applicant) => applicant.role === 'LEADER');
-    const participants = applicants.filter(
-      (applicant) => applicant.role !== 'LEADER',
-    );
-
-    const sortedParticipants = [...participants].sort(
+    const sorted = [...applicants].sort(
       (a, b) =>
         new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
     );

-    const sortedApplicants = leader
-      ? [leader, ...sortedParticipants]
-      : sortedParticipants;
-
-    return sortedApplicants.map((data) => ({
+    return sorted.map((data) => ({
       memberId: data.applicantInfo.memberId,
       nickname: data.applicantInfo.memberNickname || '익명',
       profileImageUrl:
         data.applicantInfo.profileImage?.resizedImages[0]?.resizedImageUrl ??
         '',
       isLeader: data.role === 'LEADER',
     }));
   }, [applicants]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/section/group-study-info-section.tsx` around lines 40 - 65,
The current useMemo in avatarMembers performs both createdAt sorting and moves
the leader to the front, duplicating responsibility with AvatarStack; change
avatarMembers (the useMemo block) to only sort applicants by createdAt and map
them to AvatarStackMember (keeping isLeader flag and applicantInfo fields)
without altering ordering based on leader, so AvatarStack (which already
reorders by isLeader) is solely responsible for putting the leader first; update
references in the useMemo (leader, participants, sortedParticipants,
sortedApplicants) to remove leader-based concatenation and return the
createdAt-sorted array mapped to memberId, nickname, profileImageUrl and
isLeader.
src/components/ui/avatar-stack.tsx (2)

135-183: 커스텀 툴팁에 접근성 속성이 누락되어 있습니다.

현재 hovered 상태로 직접 관리하는 커스텀 툴팁은 동작하지만, 키보드 포커스 시 표시되지 않고 role="tooltip"aria-describedby 등의 접근성 속성이 없습니다.

당장은 기능적으로 문제없으나, 접근성 개선 시 고려해주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/avatar-stack.tsx` around lines 135 - 183, The custom
tooltip in AvatarItem lacks accessibility: make the trigger element (the wrapper
div inside UserProfileModal's trigger) keyboard-focusable (add tabIndex={0}) and
wire onFocus/onBlur to the same hovered state handlers so keyboard users can
show/hide the tooltip; give the tooltip div a unique id (e.g.,
`tooltip-${member.memberId || index}`), add role="tooltip" to that div, and add
aria-describedby on the trigger element pointing to that id; keep the existing
onMouseEnter/onMouseLeave behavior so both mouse and keyboard work and use the
member.memberId/index to generate the unique id.

47-53: AvatarStack 내부에서 리더 정렬을 수행하는 점에 유의하세요.

이 컴포넌트 내부에서 리더를 앞으로 정렬하고 있는데, group-study-info-section.tsxavatarMembers 메모에서도 리더를 먼저 배치하는 정렬을 수행하고 있습니다 (Lines 43-55). 결과적으로 동일한 정렬이 두 번 실행됩니다.

정렬 책임을 한쪽으로 통일하는 것이 좋습니다. AvatarStack이 항상 리더를 앞으로 정렬한다면, consumer 쪽에서는 정렬할 필요가 없습니다. 반대로 consumer가 정렬 순서를 제어해야 한다면, AvatarStack 내부 정렬을 제거하거나 opt-out 옵션을 제공하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/avatar-stack.tsx` around lines 47 - 53, AvatarStack
currently re-sorts members to put the leader first (the sorted variable in
AvatarStack), but group-study-info-section.tsx's avatarMembers memo already
performs that leader-first sort, causing duplication; pick one side to own the
ordering and remove the other. To fix, either remove the internal sort in
AvatarStack (delete the sorted = [...members].sort(...) block and use members
as-is) and optionally add a prop like disableLeaderSort/preserveOrder to
AvatarStack if consumers may rely on its sorting, or keep AvatarStack's sort and
remove the leader-first logic from the avatarMembers memo in
group-study-info-section.tsx so only AvatarStack handles ordering. Ensure you
update any usages/test expectations of AvatarStack or avatarMembers accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/section/group-study-info-section.tsx`:
- Around line 126-127: The participant count currently uses the first-page item
array length (applicants?.length) which undercounts; update the display to use
the paginated API's totalElements instead by reading
approvedApplicants?.pages[0]?.totalElements (or fallback 0) so the UI shows the
full total; locate the render in group-study-info-section.tsx where applicants
and approvedApplicants are referenced and replace the length-based count with
the totalElements-based value.

In `@src/components/ui/avatar-stack.tsx`:
- Around line 148-150: The z-index calculation (zIndex: 10 - index) can become
negative when maxVisible > 10; update the logic to clamp z-index to a
non-negative sequence based on the actual number of visible avatars by passing a
totalVisible (or maxVisible) prop into AvatarItem (or computing it in the parent
where visible is known) and compute zIndex as a clamped value, e.g., zIndex =
Math.max(0, totalVisible - index) (or similar), or move the inline style to the
parent to use the closure's visible/totalVisible when setting marginLeft and
zIndex.
- Around line 174-179: Update the invalid Tailwind class on the tooltip div in
the AvatarStack component: replace the incorrect class name
"text-bg-background-neutral-strong" with the valid
"text-background-neutral-strong" in the JSX that renders the nickname tooltip
(the conditional block using hovered and member.nickname); leave
"bg-text-inverse" as-is so only the mistyped class is changed.

---

Duplicate comments:
In `@src/components/section/group-study-info-section.tsx`:
- Around line 35-38: The applicants variable currently picks only the first page
(approvedApplicants?.pages[0]?.content), so update the useMemo in
group-study-info-section.tsx to flatten all pages returned by
useApplicantsByStatusQuery: map/reduce/flatMap over approvedApplicants?.pages to
collect each page.content (guarding for undefined) into a single array, and keep
the dependency on approvedApplicants?.pages so the memo updates when any page
changes; reference the applicants const and approvedApplicants variable inside
the useMemo and ensure the result defaults to an empty array when no pages
exist.

---

Nitpick comments:
In `@src/components/section/group-study-info-section.tsx`:
- Around line 40-65: The current useMemo in avatarMembers performs both
createdAt sorting and moves the leader to the front, duplicating responsibility
with AvatarStack; change avatarMembers (the useMemo block) to only sort
applicants by createdAt and map them to AvatarStackMember (keeping isLeader flag
and applicantInfo fields) without altering ordering based on leader, so
AvatarStack (which already reorders by isLeader) is solely responsible for
putting the leader first; update references in the useMemo (leader,
participants, sortedParticipants, sortedApplicants) to remove leader-based
concatenation and return the createdAt-sorted array mapped to memberId,
nickname, profileImageUrl and isLeader.

In `@src/components/ui/avatar-stack.tsx`:
- Around line 135-183: The custom tooltip in AvatarItem lacks accessibility:
make the trigger element (the wrapper div inside UserProfileModal's trigger)
keyboard-focusable (add tabIndex={0}) and wire onFocus/onBlur to the same
hovered state handlers so keyboard users can show/hide the tooltip; give the
tooltip div a unique id (e.g., `tooltip-${member.memberId || index}`), add
role="tooltip" to that div, and add aria-describedby on the trigger element
pointing to that id; keep the existing onMouseEnter/onMouseLeave behavior so
both mouse and keyboard work and use the member.memberId/index to generate the
unique id.
- Around line 47-53: AvatarStack currently re-sorts members to put the leader
first (the sorted variable in AvatarStack), but group-study-info-section.tsx's
avatarMembers memo already performs that leader-first sort, causing duplication;
pick one side to own the ordering and remove the other. To fix, either remove
the internal sort in AvatarStack (delete the sorted = [...members].sort(...)
block and use members as-is) and optionally add a prop like
disableLeaderSort/preserveOrder to AvatarStack if consumers may rely on its
sorting, or keep AvatarStack's sort and remove the leader-first logic from the
avatarMembers memo in group-study-info-section.tsx so only AvatarStack handles
ordering. Ensure you update any usages/test expectations of AvatarStack or
avatarMembers accordingly.

@HA-SEUNG-JEONG HA-SEUNG-JEONG merged commit 7c519da into develop Feb 17, 2026
9 checks passed
@HA-SEUNG-JEONG HA-SEUNG-JEONG deleted the fix/study branch February 17, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant